Skip to main content

08 函数式语言的核心抽象

函数的一体两面

静态的视角来看函数,是一个函数对象(函数的实例)。函数就是用三个语义组件构成的实体:

  1. 参数:函数总是有参数的,即使它的形式参数表为空;
  2. 执行体:函数总是有它的执行过程,即使是空的函数体或空语句;
  3. 结果:函数总是有它的执行的结果,即使是 undefined。
function f() {
...
}

( )指示了参数,指示了执行体,并且函数有一个结果。

把这三个部分构成的一个整体看作执行体的时候:

(function f() {
...
})

它的结果是一个函数类型的“数据”。这在函数式语言中称为“函数是第一类型的”,也就是说函数既是可以执行的逻辑,也同时是可以被逻辑处理的数据。

函数作为数据时,它是“原始的函数声明”的一个实例。这个实例必须包括参数与执行体。否则它作为实例将是不完整的、不能准确复现原有的函数声明的。为了达到这个目的,JavaScript 为每个实例创建了一个闭包,并且作为上述“函数类型的‘数据’”的实际结果。

var arr = new Array;
for (var i=0; i<5; i++) arr.push(function f() {
// ...
});

静态的函数 f() 有且仅有一个;而在执行后,arr[] 中将存在该函数 f() 的 5 个实例,每一个称为该函数的一个运行期的闭包,它们各各不同。

两个语义组件

每个实例 / 闭包都有一个自己独立的运行环境,也就是运行期上下文。

JavaScript 中的闭包与运行环境并没有明显的语义差别,唯一不同之处,仅在于这个“运行环境”中每次都会有一套新的“参数”,且执行体的运行位置(如果有的话)被指向函数代码体的第一个指令。

函数可以多次调用,函数体和 for 的循环体(用来实现逻辑复用的执行结构)的创建技术是完全一样的。

命令式语句和函数式语言,是采用相同的方式来执行逻辑的。只不过前者把它叫做 _iteratorEnv_,是 _loopEnv_ 的实例;后者把它叫做闭包,是函数的实例。

导致 for 循环需要多个 iteratorEnv 实例的原因,在于循环语句试图在多个迭代中复用参数(迭代变量),而函数这样做的目的,也同时是为了处理这些参数(形式参数表)的复用。

在闭包创建时,参数 x 将作为闭包(作用域 / 环境)中的名字被初始化——这个过程中“参数 x”只作为名字或标识符,并且将会在闭包中登记一个名为“x”的变量;按照约定它的值是 undefined。并且这个过程是引擎为闭包初始化的,发生于用户代码得到这个闭包之前。

传入参数的处理

JavaScript 的函数是“非惰性求值”的,也就是说在函数界面上不会传入一个延迟计算的求值过程,而是“积极地”传入已经求值的结果。

// 一般函数声明
function f(x) {
console.log(x);
}

// 表达式`a=100`是“非惰性求值”的
f(a = 100);

传入函数f()的将是赋值表达式a = 100完成计算求值之后的结果。“结果”存在“值和引用”两种表达形式,JavaScript 在这里约定“传值”,上述示例代码最终执行到的将是f(100)。

a = 100这行表达式执行在函数外的上下文环境中(上例中是全局环境)。

调用这个函数 f() 时,JavaScript 才需要向环境中的那些名字(例如function f(x)中的形式参数名 x)绑定实际传入的值。对于这个x来说,由于参数与函数体使用同一个块作用域,因此如果函数参数与函数内变量同名,那么它们事实上将是同一个变量。

function f(x) {
console.log(x);
var x = 200;
}
// 由于“非惰性求值”,所以下面的代码在函数调用上完全等义于上例中`f(a = 100)`
f(100);

函数内的三个 x 实际将是同一个变量,因此这里的 console.log(x) 将显示变量 x 的传入参数值 100,而var x = 200;并不会导致“重新声明”一个变量,仅仅是覆盖了既有的 x。